package com.vision.sales.sv.serv.rpc;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.sencha.gxt.data.shared.loader.PagingLoadResult;
import com.sencha.gxt.data.shared.loader.PagingLoadResultBean;
import com.vision.core.cm.data.Domain;
import com.vision.core.cm.db.data.Company;
import com.vision.core.cm.exception.RpcException;
import com.vision.core.cm.util.NullUtils;
import com.vision.core.sv.db.mgr.IDataManager;
import com.vision.core.sv.db.mgr.ISimpleCriteria;
import com.vision.core.sv.db.mgr.SimpleCriteria;
import com.vision.core.sv.util.CollectionUtils;
import com.vision.sales.cl.ui.component.TransactionLoadConfig;
import com.vision.sales.cm.data.CollectionStatus;
import com.vision.sales.cm.data.PaymentType;
import com.vision.sales.cm.data.SalesPermissions;
import com.vision.sales.cm.data.TransactionStatus;
import com.vision.sales.cm.data.TransactionType;
import com.vision.sales.cm.db.data.Check;
import com.vision.sales.cm.db.data.Collection;
import com.vision.sales.cm.db.data.CollectionItem;
import com.vision.sales.cm.db.data.Customer;
import com.vision.sales.cm.db.data.SalesInvoice;
import com.vision.sales.cm.db.data.TransactionEntry;
import com.vision.sales.cm.serv.rpc.CollectionService;
import com.vision.sales.sv.util.TransactionHelper;

/**
 *  
 * @author Mark
 *
 */
@Service("collection")
public class CollectionServiceImpl extends TransactionServiceImpl<Collection> implements CollectionService {

	private static final long serialVersionUID = 1L;
	
	private static final Logger log = LoggerFactory.getLogger(CollectionServiceImpl.class);
	
	@Autowired
	@Qualifier("customerManager")
	private IDataManager<Customer> customerManager;
	
	@Autowired
	@Qualifier("checkManager")
	private IDataManager<Check> checkManager;
	
	@Autowired
	@Qualifier("collectionItemManager")
	private IDataManager<CollectionItem> collectionItemManager;
	
	@Autowired
	@Qualifier("salesInvoiceManager")
	private IDataManager<SalesInvoice> invoiceManager;
	
	@Autowired
	@Qualifier("transactionEntryManager")
	private IDataManager<TransactionEntry> transactionEntryManager;

	public CollectionServiceImpl() {
		initGilead();
	}
	
	@Autowired
	@Qualifier("collectionManager")
	@Override
	public void setDataManager(IDataManager<Collection> dataManager) {
		super.setDataManager(dataManager);
	}
	
	@Override
	protected Domain getDomain() {
		return Domain.COLLECTION;
	}
	
	// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	@Override
	public List<Collection> getAll() throws RpcException {
		checkPermissions(SalesPermissions.ViewCollection);
		
		return super.getAll();
	}
	
	@Transactional
	@Override
	public Collection create(Collection collection) throws RpcException {
		checkPermissions(SalesPermissions.CreateCollection);
		checkHasCompany(collection.getCompany());

		log.debug("Validating collection...");
		checkFieldUnique("refNo", collection.getRefNo(), "Reference No.");
		
		Customer customer = customerManager.getById(collection.getCustomer().getId());
		if (customer == null) throw new RpcException("The customer does not exist!");
		
		double amount = 0,
				overruns = 0,
				overpayments = 0,
				discounts = 0, rejects = 0, returns = 0,
				subtotal = 0, tax = 0, netAmount = 0;

		Set<CollectionItem> items = collection.getCollectionItems();
		
		// Retrieve the sales invoices
		Map<Long, SalesInvoice> invoicesMap = getSalesInvoices(items);
		
		for (CollectionItem item : items) {
			SalesInvoice invoice = invoicesMap.get(item.getSalesInvoice().getId());
			if (invoice == null)
				throw new RpcException("The collection references a non-existing sales invoice!");
			
			if (invoice.getStatus() != TransactionStatus.POSTED)
				throw new RpcException("The collection references an unposted sales invoice!");
			
			if (invoice.getCollectionStatus() == CollectionStatus.FULLY_PAID)
				throw new RpcException("The collection references a fully paid sales invoice!");
			
			item.setSalesInvoice(invoice);
			
			amount += NullUtils.safeDouble(item.getAmount());
			overruns += NullUtils.safeDouble(item.getOverruns());
			overpayments += NullUtils.safeDouble(item.getOverpayments());
			discounts += NullUtils.safeDouble(item.getDiscounts());
			rejects += NullUtils.safeDouble(item.getRejects());
			returns += NullUtils.safeDouble(item.getReturns());
			subtotal += NullUtils.safeDouble(item.getSubtotal());
			tax += NullUtils.safeDouble(item.getTax());
			netAmount += NullUtils.safeDouble(item.getNetAmount());
		}
		
		collection.setGrossSales(amount);
		collection.setOverruns(overruns);
		collection.setOverpayments(overpayments);
		collection.setDiscounts(discounts);
		collection.setRejects(rejects);
		collection.setReturns(returns);
		collection.setSubtotal(subtotal);
		collection.setTax(tax);
		collection.setNetAmount(netAmount);
		collection.setStatus(TransactionStatus.PENDING);
		
		log.debug("Inserting Collection to database...");
		// Create the Collection
		Collection newCollection = super.create(collection);
		
		for (CollectionItem item : items) {
			SalesInvoice invoice = invoicesMap.get(item.getSalesInvoice().getId());
			
			item.setCollection(newCollection);
			item.setSalesInvoice(invoice);
			item.setCompany(newCollection.getCompany());
			item.setCustomer(customer);
			item.setDate(newCollection.getDate());
			
			item.recompute(); // Recompute to make sure
			
			// Create the CollectionItem
			log.debug("Inserting CollectionItem {} to database...", item.getSalesInvoice().getInvoiceNo());
			collectionItemManager.create(item);
			
			// Update the SalesInvoice
			log.debug("Updating SalesInvoice {}...", invoice.getInvoiceNo());
			
			invoice.setBalance(invoice.getBalance() - item.getSubtotal());
			
			if (invoice.getBalance() <= 0.0d) {
				invoice.setCollectionStatus(CollectionStatus.FULLY_PAID);
			} else if (invoice.getBalance() < invoice.getGrossSales()) {
				invoice.setCollectionStatus(CollectionStatus.PARTIALLY_PAID);
			} else {
				invoice.setCollectionStatus(CollectionStatus.UNPAID);
			}
			
			invoiceManager.update(invoice);
			
			item.setSalesInvoice(invoice);
		}
		
		newCollection.setCollectionItems(items);
		
		logCreate(newCollection);
		
		return newCollection;
	}
	
	@Transactional
	@Override
	public Collection update(Collection collection) throws RpcException {
		checkPermissions(SalesPermissions.UpdateCollection);
		
		Collection existing = checkExisting(collection.getId());
		checkHasCompany(existing.getCompany());
		checkCanEditByStatus(existing, "collection");
		
		// Update only updatable fields
		collection.setStatus(existing.getStatus());
		
		Map<Long, CollectionItem> existingItemsMap = CollectionUtils.mapBy(existing.getCollectionItems(), "getId");
		
		Collection result = super.update(collection);
		
		List<Long> salesInvoicesId = CollectionUtils.listFrom(collection.getCollectionItems(), "getSalesInvoiceId");
		ISimpleCriteria invoiceCrit = SimpleCriteria.forClass(SalesInvoice.class);
		invoiceCrit.in("id", salesInvoicesId);
		List<SalesInvoice> invoices = findByCriteria(invoiceCrit);
		Map<Long, SalesInvoice> invoicesMap = CollectionUtils.mapBy(invoices, "getId");
		
		// Update the balances of the sales invoices
		for (CollectionItem item : collection.getCollectionItems()) {
			SalesInvoice invoice = invoicesMap.get(item.getId());
			CollectionItem existingItem = existingItemsMap.get(item.getId());

			if (existingItem != null) {
				// Add the old item's subtotal back to the invoice's balance
				invoice.setBalance(invoice.getBalance() + existingItem.getSubtotal());
			}
			
			invoice.setBalance(invoice.getBalance() - item.getSubtotal());
			
			if (invoice.getBalance() <= 0.0d) {
				invoice.setCollectionStatus(CollectionStatus.FULLY_PAID);
			} else if (invoice.getBalance() < invoice.getGrossSales()) {
				invoice.setCollectionStatus(CollectionStatus.PARTIALLY_PAID);
			} else {
				invoice.setCollectionStatus(CollectionStatus.UNPAID);
			}
			
			invoiceManager.update(invoice);
		}
		
		logUpdate(existing, result);
		
		return result;
	}
	
	@Transactional
	@SuppressWarnings("unchecked")
	@Override
	public Collection delete(Collection collection) throws RpcException {
		checkPermissions(SalesPermissions.DeleteCollection);
		
		Collection existing = checkExisting(collection.getId());
		checkHasCompany(existing.getCompany());
		checkCanDeleteByStatus(existing, "collection");
		
		// Retrieve the collection items
		ISimpleCriteria itemsCrit = SimpleCriteria.forClass(CollectionItem.class);
		itemsCrit.eq("collection.id", existing.getId());
		List<CollectionItem> items = getTemplate().findByCriteria(itemsCrit.getDetached());
		
		// Retrieve the sale invoices
		Map<Long, SalesInvoice> invoicesMap = getSalesInvoices(items);
		
		log.debug("Unlinking from sales invoices...");		
		for (CollectionItem item : items) {
			log.debug("Deleting collection item for invoice {}.", item.getSalesInvoice().getGroupAndNo());
			collectionItemManager.delete(item);
			
			// Update the sales invoice balance.
			SalesInvoice invoice = invoicesMap.get(item.getSalesInvoice().getId());
			invoice.setBalance(invoice.getBalance() + item.getSubtotal());
			invoiceManager.update(invoice);
		}
		
		Collection result = super.delete(existing);
		
		logDelete(result);
		
		return result;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public PagingLoadResult<Collection> getList(TransactionLoadConfig config) throws RpcException {
		checkPermissions(SalesPermissions.ViewCollection);
		
		config.setCompanies(new ArrayList<Company>(getLoggedInUser().getCompanies()));

		ISimpleCriteria crit = TransactionHelper.createCriteria(Collection.class, config);
		List<Collection> list = (List<Collection>) TransactionHelper.getList(crit, config, getTemplate());
		
		ISimpleCriteria countCrit = TransactionHelper.createCountCriteria(Collection.class, config);
		Long count = (Long) getTemplate().findByCriteria(countCrit.getDetached()).get(0);
		
		PagingLoadResultBean<Collection> result = new PagingLoadResultBean<Collection>(list, count.intValue(), config.getOffset());
		return result;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public List<Collection> getCollections(TransactionStatus status) throws RpcException {
		ISimpleCriteria crit = SimpleCriteria.forClass(Collection.class);
		crit.eq("status", status);
		List<Collection> list = getTemplate().findByCriteria(crit.getDetached());
		return list;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public List<CollectionItem> getCollectionItems(long collectionId) throws RpcException {
		checkPermissions(SalesPermissions.ViewCollection);
		
		Collection collection = getDataManager().getById(collectionId);
		if (collection == null) {
			log.warn("Collection with id={} was not found.", collectionId);
			throw new RpcException("The specified Collection does not exist!");
		}
		
		Company company = collection.getCompany();
		checkHasCompany(company); // Check if user is assigned to the company
		
		log.debug("Retrieving CollectionItems under Collection...");
		ISimpleCriteria itemsCrit = SimpleCriteria.forClass(CollectionItem.class);
		itemsCrit.eq("collection.id", collection.getId());
		List<CollectionItem> items = getTemplate().findByCriteria(itemsCrit.getDetached());
		return items;
	}
	
	@Transactional
	@SuppressWarnings("unchecked")
	@Override
	public List<Collection> postCollections(List<Collection> collections) throws RpcException {
		checkPermissions(SalesPermissions.PostCollection);
		
		log.debug("Retrieving existing collections...");
		List<Long> ids = CollectionUtils.listFrom(collections, "getId");
		ISimpleCriteria crit = SimpleCriteria.forClass(Collection.class);
		crit.in("id", ids);
		crit.eq("status", TransactionStatus.PENDING);
		List<Collection> existingCollections = getTemplate().findByCriteria(crit.getDetached());
		if (existingCollections.isEmpty())
			return existingCollections;
		
		List<Long> existingCollectionIds = CollectionUtils.listFrom(existingCollections, "getId");
		
		log.debug("Retrieving collection items associated with existing collections...");
		ISimpleCriteria collectionItemCrit = SimpleCriteria.forClass(CollectionItem.class);
		collectionItemCrit.in("collection.id", existingCollectionIds);
		List<CollectionItem> collectionItems = getTemplate().findByCriteria(collectionItemCrit.getDetached());
		Map<Long, List<CollectionItem>> map = new HashMap<Long, List<CollectionItem>>();
		for (CollectionItem item : collectionItems) {
			List<CollectionItem> list = map.get(item.getCollection().getId());
			if (list == null) {
				map.put(item.getCollection().getId(), list = new ArrayList<CollectionItem>());
			}
			list.add(item);
		}
		
		List<Collection> results = new ArrayList<Collection>(existingCollections.size());
		
		for (Collection existing : existingCollections) {
			checkHasCompany(existing.getCompany());
			
			if (existing.getStatus() == TransactionStatus.POSTED) {
				log.warn("Collection (Ref. No.: {}) has been posted. This will be skipped.", existing.getRefNo());
				continue;
			}

			log.debug("Posting collection {}...", existing.getRefNo());
			existing.setStatus(TransactionStatus.POSTED);
			Collection result = getDataManager().update(existing);
			
			collectionItems = map.get(existing.getId());
			
			// Create the Transaction entries
			log.debug("Creating transaction entry for collection items under {}...", existing.getRefNo());
			for (CollectionItem item : collectionItems) {
				List<TransactionEntry> entries = createForCollection(item);
				for (TransactionEntry entry : entries) {
					transactionEntryManager.create(entry);
				}
			}
			
			// Create Checks if payment type is set to CHECK
			if (existing.getPaymentType() == PaymentType.CHECK) {
				log.debug("Creating check for collection {}...", existing.getRefNo());
				Check check = new Check();
				check.setBank(existing.getBank());
				check.setCheckNo(existing.getCheckNo());
				check.setCheckDate(existing.getCheckDate());
				check.setAmount(existing.getNetAmount());
				checkManager.create(check);
			}
			
			// Log the update
			logUpdate(existing, result);
			
			results.add(result);
		}
		
		return results;
	}
	
	// Helpers %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	private Map<Long, SalesInvoice> getSalesInvoices(java.util.Collection<CollectionItem> items) {
		List<SalesInvoice> invoices = CollectionUtils.listFrom(items, "getSalesInvoice");
		List<Long> invoiceIds = CollectionUtils.listFrom(invoices, "getId");
		Map<Long, SalesInvoice> invoicesMap = CollectionUtils.mapBy(invoiceManager.getByIds(SalesInvoice.COL_ID, invoiceIds), "getId");
		return invoicesMap;
	}
	
	// Static Helpers %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	private static List<TransactionEntry> createForCollection(CollectionItem item) {
		List<TransactionEntry> list = new ArrayList<TransactionEntry>();
		
		list.add(createEntry(item, TransactionType.COLLECTION_PAYMENT, item.getAmount()));
		list.add(createEntry(item, TransactionType.COLLECTION_OVERPAYMENT, item.getOverpayments()));
		list.add(createEntry(item, TransactionType.COLLECTION_OVERRUNS, item.getOverruns()));
		list.add(createEntry(item, TransactionType.COLLECTION_DISCOUNTS, item.getDiscounts()));
		list.add(createEntry(item, TransactionType.COLLECTION_REJECTS, item.getRejects()));
		list.add(createEntry(item, TransactionType.COLLECTION_RETURNS, item.getReturns()));
		list.add(createEntry(item, TransactionType.COLLECTION_TAX, item.getTax()));
		
		return list;
	}
	
	private static TransactionEntry createEntry(CollectionItem item, TransactionType type, Double amount) {
		TransactionEntry te = new TransactionEntry();
		te.setCompany(item.getCompany());
		te.setDate(item.getDate());
		te.setType(type);
		te.setTransactionId(item.getId());
		te.setTransactionRefNo(item.getCollection().getRefNo() + " : " + item.getSalesInvoice().getGroupAndNo());
		te.setCustomer(item.getSalesInvoice().getCustomer());
		te.setSalesman(item.getSalesInvoice().getSalesman());
		te.setProduct(item.getSalesInvoice().getProduct());
		
		double amt = amount == null ? 0d : amount.doubleValue();
		
		if (type.isDebit()) {
			te.setDebit(amt);
			te.setCredit(0);
		} else {
			te.setDebit(0);
			te.setCredit(amt);
		}
		
		return te;
	}
	
}
